﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Data;
using System.Data.SqlClient;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Xml;
using System.Xml.Linq;
using System.Xml.Serialization;
using EpiNEXT.Core;
using EpiNEXT.Core.Fields;

namespace EpiNEXT.Data
{
    /// <summary>
    /// Concrete type for data stored in Xml files
    /// </summary>
    public class XmlDriver : DbDriverBase
    {
        #region Constructors
        public XmlDriver(string connectionString)
            : base(connectionString)
        {
        }

        public override string IdentifyDatabase()
        {
            return "Xml File";
        }
        #endregion // Constructors

        #region Public Methods
        public override bool SaveRecord(Form form, Record record) 
        {
            IsProcessing = true;
            ProcessingProgress = 0;
            return SaveAllRecords(form.Owner);
        }
        public override async Task<bool> SaveRecordAsync(Form form, Record record)
        {
            IsProcessing = true;
            ProcessingProgress = 0;
            Task<bool> task = SaveAllRecordsAsync(form.Owner);
            bool success = await task;
            return success;
        }
        public override bool DeleteRecord(Form form, Record record) { return false; }
        public override bool SoftDeleteRecord(Form form, Record record) { return false; }
        public override void PopulateRecordCollection(DataEntryInstrument form)
        {
            IsProcessing = true;

            form.Records.Clear();

            string tableName = form.Title;

            bool startReadingRecords = false;
            using (XmlReader xmlReader = XmlReader.Create(form.Owner.ProjectFileInfo.FullName))
            {
                while (xmlReader.Read())
                {
                    if (!startReadingRecords && (xmlReader.NodeType == XmlNodeType.Element) && (xmlReader.Name == "Form"))
                    {
                        using (XmlReader xmlSubTreeReader = xmlReader.ReadSubtree())
                        {
                            while (xmlSubTreeReader.Read())
                            {
                                if ((xmlSubTreeReader.NodeType == XmlNodeType.Element) && (xmlSubTreeReader.Name == "Id") && (xmlSubTreeReader.ReadInnerXml().Equals(form.FormId.ToString(), StringComparison.OrdinalIgnoreCase)))
                                {
                                    startReadingRecords = true;
                                }

                                if (startReadingRecords)
                                {
                                    if((xmlReader.NodeType == XmlNodeType.Element) && (xmlReader.Name == "Record"))
                                    {
                                        form.Records.Add(CreateRecord(form, xmlReader));
                                    }
                                }
                            }
                        }
                    }

                    if (!startReadingRecords && (xmlReader.NodeType == XmlNodeType.Element) && (xmlReader.Name == "DataGridTable"))
                    {
                        using (XmlReader xmlSubTreeReader = xmlReader.ReadSubtree())
                        {
                            while (xmlSubTreeReader.Read())
                            {
                                if ((xmlSubTreeReader.NodeType == XmlNodeType.Element) && (xmlSubTreeReader.Name == "Id") && (xmlSubTreeReader.ReadInnerXml().Equals(form.FormId.ToString(), StringComparison.OrdinalIgnoreCase)))
                                {
                                    startReadingRecords = true;
                                }

                                if (startReadingRecords)
                                {
                                    if ((xmlReader.NodeType == XmlNodeType.Element) && (xmlReader.Name == "Record"))
                                    {
                                        form.Records.Add(CreateRecord(form, xmlReader));
                                    }
                                }
                            }
                        }
                    }
                }
            }

            IsProcessing = false;
        }

        /// <summary>
        /// Gets an observable collection of records from a table in the database
        /// </summary>
        public override ObservableCollection<Record> GetRecordCollection(string tableName)
        {
            throw new NotImplementedException();
        }
        #endregion // Public Methods

        #region Protected Methods
        protected override bool UpdateRecord(Form form, Record record) { return false; }
        protected override bool InsertRecord(Form form, Record record) { return false; }
        #endregion // Protected Methods

        #region Private Methods

        public override bool SaveAllRecords(Project project)
        {
            SaveRecordsTask(project);
            ProcessingProgress = 0;
            IsProcessing = false;
            return true;
        }

        public override async Task<bool> SaveAllRecordsAsync(Project project)
        {
            await Task.Factory.StartNew(delegate
            {
                SaveRecordsTask(project);
            });
            
            ProcessingProgress = 0;
            IsProcessing = false;
            return true;
        }

        private bool SaveRecordsTask(Project project)
        {
            XmlSerializer serializer = new XmlSerializer(typeof(Project));
            DateTime utc = DateTime.Now.ToUniversalTime();
            string filePath = project.ProjectFileInfo.FullName;
            double maxProgress = 0;

            foreach (Form form in project.Forms)
            {
                maxProgress += form.Records.Count;
            }

            double inc = 100 / maxProgress;

            using (StreamWriter writer = new StreamWriter(filePath))
            {
                serializer.Serialize(writer, project);
            }

            XDocument doc = XDocument.Load(filePath);

            XElement root = doc.Root;

            foreach (Form form in project.Forms)
            {
                var query = (from xml in root.Descendants("Forms").Descendants("Form") 
                                where xml.Element("Id").Value.Equals(form.FormId.ToString())
                                select xml);

                foreach (XElement element in query)
                {
                    XElement recordsElement = new XElement("Records");

                    foreach (Record record in form.Records)
                    {
                        XElement recordElement = record.Serialize(doc, form);
                        recordsElement.Add(recordElement);
                        record.HasUnsavedChanges = false;
                        record.IsInDatabase = true;
                        ProcessingProgress = ProcessingProgress + inc;
                    }

                    element.Add(recordsElement);
                }
            }

            foreach (DataGridTable dataGridTable in project.DataGridTables)
            {
                var query = (from xml in root.Descendants("DataGridTables").Descendants("DataGridTable")
                             where xml.Attribute("Id").Value.Equals(dataGridTable.FormId.ToString())
                             select xml);

                foreach (XElement element in query)
                {
                    XElement recordsElement = new XElement("Records");

                    foreach (Record record in dataGridTable.Records)
                    {
                        XElement recordElement = record.Serialize(doc, dataGridTable);
                        recordsElement.Add(recordElement);
                        record.HasUnsavedChanges = false;
                        record.IsInDatabase = true;
                    }

                    element.Add(recordsElement);
                }
            }

            doc.Save(filePath);
            doc = null;

            return true;
        }

        /// <summary>
        /// Creates a dynamic Record object from a record element in an Xml file
        /// </summary>
        /// <param name="form">The form to which the record belongs</param>
        /// <param name="xmlReader">The current record being read in the Xml file</param>
        /// <returns>The record observable object</returns>
        private dynamic CreateRecord(DataEntryInstrument form, XmlReader xmlReader)
        {
            dynamic record = new Record();
            record.Id = new Guid("00000000-0000-0000-0000-000000000000");
            record.Initialize(form);
            record.IsInDatabase = true;

            if (xmlReader.HasAttributes)
            {
                record.Id = new Guid(xmlReader.GetAttribute("Id"));
                record.RecStatus = Convert.ToByte(xmlReader.GetAttribute("RecStatus"));
                record.FirstSaveTime = new DateTime(Convert.ToInt64(xmlReader.GetAttribute("FirstSaveTime")));
                record.LastSaveTime = new DateTime(Convert.ToInt64(xmlReader.GetAttribute("LastSaveTime")));
                record.FirstSaveLogOn = xmlReader.GetAttribute("FirstSaveLogon");
                record.LastSaveLogOn = xmlReader.GetAttribute("LastSaveLogon");
                if (xmlReader.GetAttribute("FKey") != null)
                {
                    record.ForeignKey = new Guid(xmlReader.GetAttribute("FKey"));
                }
            }

            List<string> usedFieldNames = new List<string>();

            using (XmlReader xmlSubTreeReader = xmlReader.ReadSubtree())
            {
                XElement recordElement = XElement.Load(xmlSubTreeReader);

                foreach (XElement dataElement in recordElement.Descendants("D"))
                {
                    string fieldName = dataElement.Attribute("N").Value;
                    SetFieldValue(form, record, fieldName, dataElement.Value /*xmlReader*/);
                    usedFieldNames.Add(fieldName);
                }
            }

            // This is for performance reasons; lookups on properties that don't exist are very, very expensive, so we
            // just set all missing data to null. (e.g. you have a field called 'Name' but the XML doesn't have a <Data>
            // element for 'Name', because it was left blank perhaps, therefore it's not read into memory as part of the
            // Xml reader code above.)
            foreach (IField field in form.Fields)
            {
                if (field is IInputField && !usedFieldNames.Contains(field.Name))
                {
                    record.AddOrUpdateFieldData(field, null);
                }
            }

            record.HasUnsavedChanges = false;
            record.SuppressErrorNotification = false;

            return record;
        }

        /// <summary>
        /// Sets the value for the record object
        /// </summary>
        /// <param name="form">The form to which the record belongs</param>
        /// <param name="record">The record whose value will be set</param>
        /// <param name="fieldName">The name of the field that will be updated in this record</param>
        /// <param name="reader">The row in the Xml file that contains the value to be assigned</param>
        [Obsolete("Method should be avoided; use Xml-to-Linq-based method of the same name instead.", true)]
        private void SetFieldValue(DataEntryInstrument form, Record record, string fieldName, XmlReader reader)
        {
            #region Input Validation
            if (fieldName == null)
            {
                throw new ArgumentNullException("fieldName");
            }
            #endregion // Input Validation

            string innerXml = reader.ReadInnerXml().Trim();

            SetFieldValue(form, record, fieldName, innerXml);
        }

        /// <summary>
        /// Sets the value for the record object
        /// </summary>
        /// <param name="form">The form to which the record belongs</param>
        /// <param name="record">The record whose value will be set</param>
        /// <param name="fieldName">The name of the field that will be updated in this record</param>
        /// <param name="innerXml">The Xml string that represents the value</param>
        private void SetFieldValue(DataEntryInstrument form, Record record, string fieldName, string innerXml)
        {
            #region Input Validation
            if (fieldName == null)
            {
                throw new ArgumentNullException("fieldName");
            }
            #endregion // Input Validation

            if (form.Fields.Contains(fieldName))
            {
                IField field = form.Fields[fieldName];
                string fieldType = field.GetType().ToString();

                if (!String.IsNullOrEmpty(innerXml))
                {
                    switch (fieldType)
                    {
                        case "EpiNEXT.Core.Fields.ImageField":
                            if (!String.IsNullOrEmpty(innerXml.Trim())) 
                            { 
                                byte[] imageBytes = Convert.FromBase64String(innerXml);
                                record.AddOrUpdateFieldData(field, imageBytes);
                            }
                            else
                            {
                                record.AddOrUpdateFieldData(field, null);
                            }
                            break;
                        case "EpiNEXT.Core.Fields.NumberField":
                            decimal decimalFieldValue;

                            bool decimalParseSuccess = decimal.TryParse(innerXml, out decimalFieldValue);
                            if (decimalParseSuccess)
                            {
                                record.AddOrUpdateFieldData(field, decimalFieldValue);
                            }
                            else
                            {
                                record.AddOrUpdateFieldData(field, null);
                            }
                            break;
                        case "EpiNEXT.Core.Fields.DatePickerField":
                        case "EpiNEXT.Core.Fields.DateBoxField":
                            long longFieldValue;

                            bool dateParseSuccess = long.TryParse(innerXml, out longFieldValue);
                            if (dateParseSuccess)
                            {
                                DateTime dateFieldValue = new DateTime(longFieldValue);

                                if (field is DatePickerField)
                                {
                                    record.AddOrUpdateFieldData(field, dateFieldValue);
                                }
                                else if (field is DateBoxField)
                                {
                                    DateBoxField dboxField = field as DateBoxField;

                                    string mask = dboxField.InputMask;
                                    

                                    switch (dboxField.FormatMode)
                                    {
                                        case DateFormatMode.Default:
                                            string dateFormat = System.Globalization.CultureInfo.CurrentUICulture.DateTimeFormat.ShortDatePattern;
                                            if (dateFormat.StartsWith("M"))
                                            {
                                                mask = "MM/dd/yyyy HH:mm:ss";
                                            }
                                            else
                                            {
                                                mask = "dd/MM/yyyy HH:mm:ss";
                                            }
                                            break;
                                        case DateFormatMode.DayMonthYear:
                                            mask = "dd/MM/yyyy HH:mm:ss";
                                            break;
                                        case DateFormatMode.MonthDayYear:
                                            mask = "MM/dd/yyyy HH:mm:ss";
                                            break;
                                        case DateFormatMode.YearMonthDay:
                                            mask = "yyyy/MM/dd HH:mm:ss";
                                            break;
                                    }

                                    string recordValue = dateFieldValue.ToString(mask);
                                    record.AddOrUpdateFieldData(field, recordValue);
                                }
                            }
                            else
                            {
                                record.AddOrUpdateFieldData(field, null);
                            }
                            break;
                        case "EpiNEXT.Core.Fields.CheckBoxField":
                            bool checkBoxFieldValue;

                            bool checkBoxParseSuccess = Boolean.TryParse(innerXml, out checkBoxFieldValue);
                            if (checkBoxParseSuccess)
                            {
                                record.AddOrUpdateFieldData(field, checkBoxFieldValue);
                            }
                            else
                            {
                                record.AddOrUpdateFieldData(field, null);
                            }
                            break;
                        case "EpiNEXT.Core.Fields.FixedCellTextBoxField":
                            string charValues = innerXml;

                            FixedCellTextBoxField fixedCellTextBoxField = field as FixedCellTextBoxField;
                            int cells = fixedCellTextBoxField.Cells;

                            ObservableCollection<CharacterContainer> characters = new ObservableCollection<CharacterContainer>();
                            foreach (char c in charValues)
                            {
                                CharacterContainer container = new CharacterContainer();
                                container.Character = c;
                                characters.Add(container);
                            }

                            // add extra padding to get to cell count
                            for (int i = characters.Count; i < cells; i++)
                            {
                                characters.Add(new CharacterContainer());
                            }

                            record.AddOrUpdateFieldData(field, characters);
                            break;
                        default:
                            record.AddOrUpdateFieldData(field, innerXml);
                            break;
                    }
                }
                else
                {
                    if (fieldType.Equals("EpiNEXT.Core.Fields.FixedCellTextBoxField"))
                    {
                        ObservableCollection<CharacterContainer> characters = new ObservableCollection<CharacterContainer>();

                        for (int i = 0; i < (field as FixedCellTextBoxField).Cells; i++)
                        {
                            characters.Add(new CharacterContainer());
                        }

                        record.AddOrUpdateFieldData(field, characters);
                    }
                    else
                    {
                        record.AddOrUpdateFieldData(field, null);
                    }
                }
            }
        }
        #endregion // Private Methods
    }
}
